Skip to content

feat(container-runtime): expose versionMarkResolver on IContainerRuntime - #28168

Open
lindsnguyen wants to merge 7 commits into
microsoft:mainfrom
lindsnguyen:version-marks-resolver-access-point
Open

feat(container-runtime): expose versionMarkResolver on IContainerRuntime#28168
lindsnguyen wants to merge 7 commits into
microsoft:mainfrom
lindsnguyen:version-marks-resolver-access-point

Conversation

@lindsnguyen

@lindsnguyen lindsnguyen commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Description

Expose the version mark resolver as a supported access point on the @legacy @beta IContainerRuntimeBase interface, so hosts obtain it from a supported interface instead of casting the concrete @internal ContainerRuntime class. Fixes AB#82258.

  • Adds versionMarkResolver: IVersionMarkResolver to IContainerRuntimeBase.
  • Moves the resolver public types (IVersionMarkResolver, ResolveResult, VersionMarkCapture) into @fluidframework/runtime-definitions (the package that owns IContainerRuntimeBase). @fluidframework/container-runtime re-exports them, so existing consumers importing from @fluidframework/container-runtime/legacy are unchanged.
  • ResolveResult's pending and unresolvable results carry an optional reason?: string, an opaque log-only diagnostic. Hosts drive behavior from kind. reason is not a contract and must not be branched on.

Why the resolver moved from IContainerRuntime to IContainerRuntimeBase

This work started by exposing versionMarkResolver on IContainerRuntime. During review we found that the actual host consumer (office-bohemia) reaches the runtime from inside a DataObject, through IFluidDataStoreContext.containerRuntime, which is typed IContainerRuntimeBase, not IContainerRuntime. A data store is only ever handed the base surface by contract, so exposing the resolver on IContainerRuntime alone would force the consumer to keep an unsafe as IContainerRuntime downcast past its own contract.

Placing the member on IContainerRuntimeBase (which IContainerRuntime extends, so host-level callers still see it) lets the real consumer read it type-safely and removes the shim entirely. This also matches the role of the base as the data-store-facing service surface.

How this removes the office-bohemia shim

Because the resolver was not on any importable interface, office-bohemia consumes it today through a structural duck-typing shim (packages/base-container/src/VersionMarkService.ts):

// Before: untyped param and a runtime probe, because versionMarkResolver
// is not on IContainerRuntimeBase (what a DataObject can see).
interface VersionMarkCapableRuntime {
  readonly versionMarkResolver: IVersionMarkResolver;
}
function isVersionMarkCapableRuntime(runtime: object): runtime is VersionMarkCapableRuntime {
  return 'versionMarkResolver' in runtime && runtime.versionMarkResolver !== undefined;
}
export function createVersionMarkService(runtime: object, ...) {
  if (!isVersionMarkCapableRuntime(runtime)) {
    throw new Error('The container runtime does not support durable version marks.');
  }
  return new PersistentVersionMarkService(runtime.versionMarkResolver, ...);
}

// Caller (RootComponent, a DataObject): context.containerRuntime is IContainerRuntimeBase.
createVersionMarkService(this.context.containerRuntime, ...);

With the resolver on IContainerRuntimeBase, that collapses to a typed access with no probe and no cast:

// After: the parameter is the real interface, and the property is typed.
export function createVersionMarkService(runtime: IContainerRuntimeBase, ...) {
  return new PersistentVersionMarkService(runtime.versionMarkResolver, ...);
}

// Caller is unchanged, and now type-safe end to end.
createVersionMarkService(this.context.containerRuntime, ...);

The office-bohemia cleanup is a separate follow-up. This PR only makes it possible. It does not break office-bohemia before they take it: their imports still resolve, and their existing runtime probe keeps working because it duck-types the concrete runtime rather than a declared type.

Reviewer Guidance

  • The resolver is on IContainerRuntimeBase rather than IContainerRuntime on purpose: the supported consumer is a DataObject that only sees IContainerRuntimeBase via IFluidDataStoreContext.containerRuntime. Exposing it only on IContainerRuntime would leave that consumer casting.
  • Why this needs no new type-test acknowledgment: IContainerRuntimeBase and IContainerRuntime are both @sealed, so their type-tests only run the backCompat direction (a current instance must satisfy the older type). Adding a required member keeps current assignable to the narrower old, so that direction still passes. The forward-compat direction (where a new required member would break an old implementer) is not run for sealed interfaces, so no acknowledgment is required. Moving the member between the two sealed interfaces produces no type-test diff.
  • The one acknowledged break is unchanged from the original access-point commit: the non-sealed, deprecated IContainerRuntimeWithResolveHandle_Deprecated runs both directions, so its forward-compat break stays acknowledged via typeValidation.broken.
  • The types now live in @fluidframework/runtime-definitions because IContainerRuntimeBase lives there and cannot depend on container-runtime-definitions. container-runtime re-exports them so @fluidframework/container-runtime/legacy consumers are unchanged.
  • reason?: string is deliberately a plain string, not a string-literal union, so it stays diagnostic-only and additions never break exhaustive kind consumers.

Copilot AI lite review requested due to automatic review settings September 3, 2026 20:37
@lindsnguyen
lindsnguyen requested review from a team as code owners September 3, 2026 20:37
@github-actions github-actions Bot added area: tools area: runtime Runtime related issues area: repo Repo related work area: website public api change Changes to a public API changeset-present base: main PRs targeted against main branch labels Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Hi! Thank you for opening this PR. Want me to review it?

Based on the diff (445 lines, 15 files), I've queued these reviewers:

  • Correctness — logic errors, race conditions, lifecycle issues
  • Security — vulnerabilities, secret exposure, injection
  • API Compatibility — breaking changes, release tags, type design
  • Performance — algorithmic regressions, memory leaks
  • Testing — coverage gaps, hollow tests

How this works

  • Adjust the reviewer set by ticking/unticking boxes above. Reviewer toggles alone don't trigger anything.

  • Tick Start review below to dispatch the review fleet.

  • After review finishes, tick Start review again to request another run — it auto-resets after each dispatch.

  • This comment updates as new commits land; your reviewer selections are preserved.

  • Start review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

@fluidframework/container-runtime currently re-exports the version-mark types from @fluidframework/container-runtime-definitions/internal, which conflicts with the stated intent to re-export from /legacy and may leak an internal module specifier into the public .d.ts surface.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR graduates the version mark resolver to a supported host-facing access point by adding versionMarkResolver to the @legacy @beta IContainerRuntime interface, moving the related public types into @fluidframework/container-runtime-definitions, and extending ResolveResult with an optional diagnostic reason?: string for pending/unresolvable.

Changes:

  • Move IVersionMarkResolver, ResolveResult, and VersionMarkCapture into @fluidframework/container-runtime-definitions and export them from the package entrypoint.
  • Add versionMarkResolver: IVersionMarkResolver to IContainerRuntime so hosts no longer need to cast to ContainerRuntime.
  • Add an optional reason?: string to ResolveResult’s pending/unresolvable outcomes and update runtime + tests/docs accordingly.
File summaries
File Description
packages/runtime/container-runtime/src/versionMarks/versionMarkResolver.ts Switches to imported shared public types and adds reason when returning pending/unresolvable.
packages/runtime/container-runtime/src/versionMarks/index.ts Re-exports version mark public types from container-runtime-definitions and continues exporting the implementation.
packages/runtime/container-runtime/src/versionMarks/DEV.md Updates design notes to document the reason?: string policy and improves formatting.
packages/runtime/container-runtime/src/test/versionMarks/versionMarkResolver.spec.ts Updates expected ResolveResult shapes to include reason where applicable.
packages/runtime/container-runtime/src/test/containerRuntime.spec.ts Updates the loader-compat regression test to expect reason: "historicalOpsUnavailable".
packages/runtime/container-runtime/api-report/container-runtime.legacy.beta.api.md Updates legacy beta API report for ResolveResult.reason?: string.
packages/runtime/container-runtime/api-report/container-runtime.legacy.alpha.api.md Updates legacy alpha API report for ResolveResult.reason?: string.
packages/runtime/container-runtime-definitions/src/versionMarks.ts Introduces the new shared @legacy @beta version mark API types and docs.
packages/runtime/container-runtime-definitions/src/test/types/validateContainerRuntimeDefinitionsPrevious.generated.ts Updates type-test baseline to acknowledge expected forward-compat break for the deprecated interface.
packages/runtime/container-runtime-definitions/src/index.ts Re-exports the new version mark types from the package root.
packages/runtime/container-runtime-definitions/src/containerRuntime.ts Adds versionMarkResolver to IContainerRuntime.
packages/runtime/container-runtime-definitions/package.json Marks the known forward-compat break for the deprecated interface in typeValidation.broken.
packages/runtime/container-runtime-definitions/api-report/container-runtime-definitions.legacy.beta.api.md Adds the new IVersionMarkResolver/ResolveResult/VersionMarkCapture exports and IContainerRuntime.versionMarkResolver.
.changeset/version-mark-resolver-access-point.md Adds changeset for the API surface move and ResolveResult.reason.
Review details
  • Files reviewed: 14/14 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/runtime/container-runtime/src/versionMarks/index.ts Outdated
@anthony-murphy

Copy link
Copy Markdown
Contributor

packages/runtime/container-runtime/src/versionMarks/versionMarkResolver.ts:185

Deep Review: The diagnostic reason is set on returned results — historicalOpsUnavailable at :152, awaitingSequence at :259/:272, historyTrimmed at :265/:269 — but it never reaches the "Resolve" telemetry event. The finally block builds { eventName: "Resolve", outcome, path, durationMs, ...(resolvedSequenceNumber === undefined ? {} : { sequenceNumber }) } with no reason. versionMarkResolver.spec.ts:946-958 confirms the gap: a no-reader resolve returns { kind: "pending", reason: "historicalOpsUnavailable" } while the matching telemetry expectation carries only eventName/outcome/path.

Consequence: the runtime never logs why a mark did not resolve — every host must forward the field itself. The value is low-cardinality and PII-free, so add it to the payload under the same "diagnostic-only, unstable, log-only" rule already applied to the returned field, and update the shape assertion in the spec. If the omission is intentional (cardinality/PII), document it as a non-goal instead — and state the supported way for the runtime, not each host, to log the non-resolution cause.

@anthony-murphy

Copy link
Copy Markdown
Contributor

packages/runtime/container-runtime/src/versionMarks/DEV.md:92

Deep Review: This PR adds readonly versionMarkResolver: IVersionMarkResolver to the @legacy @beta @sealed IContainerRuntime and makes the concrete getter public (no @internal), but DEV.md — a file this PR edits — still describes the old state. Lines 92-95 say the getter is @internal and that "A future public API may move this onto container-runtime definitions rather than the concrete runtime class" — that future move is exactly what this PR does. Line 122 ("Get the resolver: ContainerRuntime.versionMarkResolver") names the superseded access point.

Rewrite the "Host exposure" paragraph (lines 92-95) to state the resolver is now exposed on the @legacy @beta IContainerRuntime interface — drop the @internal wording and the "future public API may move this" sentence — and change line 122 to reference IContainerRuntime.versionMarkResolver.

@dannimad Daniel Madrid (dannimad) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not merge until bump PR is merged first

@lindsnguyen
lindsnguyen force-pushed the version-marks-resolver-access-point branch from 42ca62f to dd89cdc Compare September 3, 2026 23:43
Comment thread .changeset/version-mark-resolver-access-point.md Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving docs changes. I didn't review the code or API changes in much detail (I see that Tony has already been reviewing those).

@lindsnguyen
lindsnguyen force-pushed the version-marks-resolver-access-point branch 2 times, most recently from 542b451 to 6d76d41 Compare September 8, 2026 23:52
Comment thread packages/runtime/container-runtime-definitions/src/versionMarks.ts Outdated
Comment thread packages/runtime/container-runtime-definitions/src/versionMarks.ts
* Licensed under the MIT License.
*/

export type {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we going to remove these exports after moving consumers over to import directly from the definitions package? (Will be moot if we go with a free function that extracts the IVersionMarkResolver from an IContainerRuntime like my other comment suggests - just export that function from here along with these types)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah we'll remove it after the shim on the office bohemia side is removed during a release where beta breaking changes (maybe 3.10?). I filed a workitem: https://dev.azure.com/fluidframework/internal/_workitems/edit/82953 so we don't forget

@lindsnguyen
lindsnguyen force-pushed the version-marks-resolver-access-point branch from 1a927fc to 4a8733e Compare September 9, 2026 20:44
Comment thread packages/runtime/container-runtime-definitions/src/versionMarks.ts Outdated
* typeValidation.broken:
* "Interface_IContainerRuntimeWithResolveHandle_Deprecated": {"forwardCompat": false}
*/
// @ts-expect-error compatibility expected to be broken

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of date?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still needed, not out of date. IContainerRuntimeWithResolveHandle_Deprecated is non-sealed and extends IContainerRuntime, so it transitively inherits the new versionMarkResolver member, and its forward-compat direction breaks against the previous release. I confirmed by removing the @ts-expect-error and rebuilding: the type-test fails with an assignability error on the old_as_current check.

Comment thread packages/runtime/container-runtime-definitions/src/containerRuntime.ts Outdated
"typeValidation": {
"broken": {},
"broken": {
"Interface_IContainerRuntimeWithResolveHandle_Deprecated": {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above, we need this since it's not sealed.

lindsnguyen and others added 7 commits September 9, 2026 17:17
Move the version mark resolver public types (IVersionMarkResolver,
ResolveResult, VersionMarkCapture) into container-runtime-definitions and
expose `versionMarkResolver` on the `@legacy @beta` IContainerRuntime
interface, so hosts obtain the resolver from a supported interface rather
than the concrete `@internal` ContainerRuntime class. container-runtime
re-exports the types for back-compat; its API surface is unchanged.
…nding/unresolvable ResolveResult

- `reason` is an opaque, log-only diagnostic string, not a typed union. The runtime
  currently sets `awaitingSequence` / `historicalOpsUnavailable` on `pending` and
  `historyTrimmed` on `unresolvable`.
- Hosts drive all behavior from `kind` and must not branch on `reason`. A plain string
  keeps additions non-breaking and avoids a second de facto discriminator; a future
  state needing different behavior should be a new `kind`, not a new `reason`.
- `reason` is transient operational context, not persisted. office-bohemia will log it
  when moving to the supported access point but makes no behavioral change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@lindsnguyen
lindsnguyen force-pushed the version-marks-resolver-access-point branch from 0a2cbef to 6c241d7 Compare September 10, 2026 00:17
@github-actions

Copy link
Copy Markdown
Contributor

🔗 No broken links found! ✅

Your attention to detail is admirable.

linkcheck output

$ start-server-and-test "npm run serve -- --host 127.0.0.1 --no-open" http://127.0.0.1:3000 check-links
1: starting server using command "npm run serve -- --host 127.0.0.1 --no-open"
and when url "[ 'http://127.0.0.1:3000' ]" is responding with HTTP status code 200
running tests using command "npm run check-links"


> fluid-framework-website@0.0.0 serve
> docusaurus serve --host 127.0.0.1 --no-open

[SUCCESS] Serving "build" directory at: http://127.0.0.1:3000/

> fluid-framework-website@0.0.0 check-links
> linkcheck http://127.0.0.1:3000 --skip-file skipped-urls.txt

Crawling...

Stats:
  351523 links
    2064 destination URLs
    2323 URLs ignored
       0 warnings
       0 errors


@anthony-murphy

Copy link
Copy Markdown
Contributor

Deep Review

Reviewed commit 6c241d7 on 2026-09-09.

Readiness: 10/10 — READY

Ready for human review. The redundant compatibility re-export file from the prior review is removed; the supported IContainerRuntimeBase.versionMarkResolver access path, compatibility handling, diagnostics, documentation, and focused tests are coherent.

Context for Reviewers

For human reviewer
  • Needs human judgment — Confirm the documented choice to keep reason?: string opaque and log-only, with kind as the only behavioral discriminator.
  • Needs human judgment — Confirm IContainerRuntimeBase is the right long-term location versus a future nested runtime-services surface; the direct data-store access requirement supports the current incremental choice.
  • Needs human judgment — Confirm the planned later removal of container-runtime compatibility re-exports after the office-bohemia migration fits beta API lifecycle policy; the author linked the tracking work item in the existing thread.
  • Cannot be assessed by the pipeline — Confirm the scoped typeValidation.broken entry for IContainerRuntimeWithResolveHandle_Deprecated follows repository release policy.
  • Cannot be assessed by the pipeline — Verify the client-package build and final bundle-size report complete successfully.
Review history (5 prior reviews)
  • 90854e7 2026-09-09 · 9/10 — One contained source cleanup remains before sign-off: remove the unreachable compatibility re-export file flagged inline.
  • 6d76d41 2026-09-08 · 10/10 — Ready for human review with no author-owned changes remaining.
  • 542b451 2026-09-08 · 10/10 — Ready for human review with no author-owned changes remaining.
  • dd89cdc 2026-09-04 · 9/10 — Ready for human review — no blocking defects.
  • 1015bcd 2026-09-03 · 8/10 — four doc/telemetry polish items flagged inline

@github-actions

Copy link
Copy Markdown
Contributor

Bundle size comparison

Base commit: 44e6a4cdf12e0f4dd0cc0e2753f22aa521ecd125
Head commit: 6c241d74d69ef52d8775772f34288c5c9e27d387

Notable changes

No bundles changed by ≥ 500 bytes parsed.

Per-bundle deltas

@fluid-example/bundle-size-tests

  • fluidFrameworkAllAlpha.js: parsed 804364 → 804420 (+56), gzip 220823 → 220881 (+58)
  • azureClient.js: parsed 634027 → 634199 (+172), gzip 169872 → 170010 (+138)
  • odspClient.js: parsed 605273 → 605561 (+288), gzip 162673 → 162866 (+193)
  • aqueduct.js: parsed 537904 → 538092 (+188), gzip 144348 → 144451 (+103)
  • fluidFramework.js: parsed 413678 → 413711 (+33), gzip 117282 → 117286 (+4)
  • sharedTree.js: parsed 403057 → 403083 (+26), gzip 114718 → 114724 (+6)
  • containerRuntime.js: parsed 314719 → 314878 (+159), gzip 86334 → 86383 (+49)
  • sharedString.js: parsed 175191 → 175198 (+7), gzip 49636 → 49644 (+8)
  • experimentalSharedTree.js: parsed 161846 → 161846 (0), gzip 46722 → 46722 (0)
  • matrix.js: parsed 153720 → 153727 (+7), gzip 44381 → 44388 (+7)
  • loader.js: parsed 147327 → 147343 (+16), gzip 40038 → 40048 (+10)
  • odspDriver.js: parsed 105689 → 105747 (+58), gzip 32932 → 33000 (+68)
  • directory.js: parsed 65669 → 65676 (+7), gzip 18493 → 18501 (+8)
  • 578.js: parsed 58686 → 58686 (0), gzip 17657 → 17657 (0)
  • odspPrefetchSnapshot.js: parsed 45921 → 45902 (-19), gzip 15346 → 15357 (+11)
  • map.js: parsed 45820 → 45827 (+7), gzip 14119 → 14126 (+7)
  • 252.js: parsed 44384 → 44384 (0), gzip 13741 → 13741 (0)
  • summarizerDelayLoadedModule.js: parsed 31287 → 31287 (0), gzip 7929 → 7929 (0)
  • socketModule.js: parsed 26992 → 26962 (-30), gzip 8017 → 8052 (+35)
  • createNewModule.js: parsed 12464 → 12464 (0), gzip 4792 → 4805 (+13)
  • summaryModule.js: parsed 3888 → 3888 (0), gzip 1874 → 1874 (0)
  • connectionState.js: parsed 909 → 909 (0), gzip 500 → 500 (0)
  • sharedTreeAttributes.js: parsed 845 → 852 (+7), gzip 496 → 505 (+9)
  • debugAssert.js: parsed 429 → 429 (0), gzip 299 → 299 (0)
  • FluidFramework-HashFallback.js: parsed 419 → 419 (0), gzip 313 → 313 (0)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: repo Repo related work area: runtime Runtime related issues area: tools area: website base: main PRs targeted against main branch changeset-present deep-review public api change Changes to a public API

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants